﻿using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using Math = System.Math;
using System.Linq;

public class MapGenerator : MonoBehaviour
{

	public GameObject Wall;
	public GameObject Floor;
	public GameObject Enemy;

	void Start()
	{
		GenerateNew();
	}

	void Update()
	{
		//if (Input.GetKeyUp(KeyCode.F5))
		//	GenerateNew();
	}

	public const float BLOCK_SIZE = 0.5f;

	private int[,] _world = null;
	private int _dungeonWidth = 0;
	private int _dungeonHeight = 0;

	private void GenerateNew()
	{
		//Random.seed = 1;
		_dungeonWidth = Random.Range(30, 164);
		_dungeonHeight = Random.Range(30, 164);

		_world = new int[_dungeonWidth, _dungeonHeight];

		var minRoomSize = 5;
		var maxRoomSize = 15;

		//var aux = (_dungeonWidth + _dungeonHeight);
		//var roomsCount = Random.Range(aux / 10, aux / 5);

		var roomsCount = Random.Range(10, 35);
		var roomList = new List<Room>();

		BuildRooms(minRoomSize, maxRoomSize, roomsCount, roomList);

		Debug.Log(roomList.Count + " (" + roomsCount + ") rooms in " + _dungeonWidth + "x" + _dungeonHeight);

		SquashRooms(roomList);

		MakeCorridors(roomList);

		FillRooms(roomList);

		if (!new RoomHelper(_world).AreAllRoomsConnected(roomList[0].X, roomList[0].Y))
		{
			Debug.Log("!!ISOLATED ROOMS!! Aborting... Generating new...");
			GenerateNew();
			return;
		}

		MakeWalls();

		for (int i = 0; i < _dungeonWidth; i++)
		{
			for (int j = 0; j < _dungeonHeight; j++)
			{
				//if (_world[i, j] == 0) continue;
				var pos = new Vector2(i, j);
				var x = (pos.x * BLOCK_SIZE);
				var y = (pos.y * BLOCK_SIZE);

				if (_world[i, j] == 1 || _world[i, j] == 3)
				{
					//path & room
					var obj = Instantiate(Floor, new Vector3(x, y * -1, 0), Quaternion.identity) as GameObject;
					if (_world[i, j] == 3)
						obj.renderer.material.color = Color.red;
				}
				else if (_world[i, j] == 2)
				{
					//wall
					var obj = Instantiate(Wall, new Vector3(x, y * -1, 0), Quaternion.identity) as GameObject;
				}
				else
				{
					//resto
					var obj = Instantiate(Wall, new Vector3(x, y * -1, 0), Quaternion.identity) as GameObject;
					obj.renderer.material.color = Color.magenta;
				}
			}
		}

		SpawnPlayer(roomList);
	}

	private void SpawnPlayer(List<Room> roomList)
	{
		var r = roomList[0];
		var x = r.X + 1;
		var y = r.Y + 1;
		var player = GameObject.Find("Player") as GameObject;
		player.transform.position = new Vector2(x * BLOCK_SIZE, y * BLOCK_SIZE * -1);

		//start room
		var obj = Instantiate(Floor, new Vector3(x * BLOCK_SIZE, y * BLOCK_SIZE * -1, 0), Quaternion.identity) as GameObject;
		obj.renderer.material.color = Color.green;
		obj.renderer.sortingOrder = 3;


		//finish room
		r = (from A in roomList where A.ConnectedTo == null select A).FirstOrDefault();
		x = r.X + 1;
		y = r.Y + 1;
		obj = Instantiate(Floor, new Vector3(x * BLOCK_SIZE, y * BLOCK_SIZE * -1, 0), Quaternion.identity) as GameObject;
		obj.renderer.material.color = Color.red;
		obj.renderer.sortingOrder = 3;
	}

	private void BuildRooms(int min, int max, int roomsCount, List<Room> roomList)
	{
		int tries = 5000000;

		for (int i = 0; i < roomsCount; i++)
		{
			var x = Random.Range(1, _dungeonWidth - max - 1);
			var y = Random.Range(1, _dungeonHeight - max - 1);

			var rw = Random.Range(min, max);
			var rh = Random.Range(min, max);
			var room = new Room(x, y, rw, rh);

			if (DoesCollide(roomList, room))
			{
				//Debug.Log(room);
				i--;
				if (tries <= 0)
					return;
				tries--;
				continue;
			}

			room.Width--;
			room.Height--;

			roomList.Add(room);
			tries = 15;
		}
	}

	private bool IsValidPosition(int x, int y)
	{
		if (x >= _dungeonWidth) return false;
		if (x < 0) return false;

		if (y >= _dungeonHeight) return false;
		if (y < 0) return false;

		return true;
	}

	private void MakeWalls()
	{
		for (var x = 0; x < _dungeonWidth; x++)
		{
			for (var y = 0; y < _dungeonHeight; y++)
			{
				if (_world[x, y] == 1)
				{
					for (var xx = x - 1; xx <= x + 1; xx++)
					{
						for (var yy = y - 1; yy <= y + 1; yy++)
						{
							if (_world[xx, yy] == 0) _world[xx, yy] = 2;
						}
					}
				}
			}
		}
	}

	private void FillRooms(List<Room> roomList)
	{
		foreach (var room in roomList)
		{
			for (int i = room.X; i < room.Width + room.X; i++)
			{
				for (int j = room.Y; j < room.Height + room.Y; j++)
				{
					_world[i, j] = 1;
				}
			}

            int n = Random.Range(0, (room.Width + room.Height) / 4);
            for (int i = 0; i < n; i++)
            {
                var x = Random.Range(room.X + 1, room.Right - 1);
                var y = Random.Range(room.Y + 1, room.Bottom - 1);
                var obj = Instantiate(Enemy, new Vector3(x * BLOCK_SIZE, y * BLOCK_SIZE * -1, 0), Quaternion.identity) as GameObject;
                obj.renderer.sortingOrder = 3;
            }
		}
	}

	//private void MakeCorridors(List<Room> roomList, bool checkIfConnected = false)
	//{
	//	for (int i = 0; i < roomList.Count; i++)
	//	{
	//		var roomA = roomList[i];
	//		var roomB = FindClosestRoom(roomA, roomList, checkIfConnected);

	//		var pointA = new Point(Random.Range(roomA.X, roomA.X + roomA.Width), Random.Range(roomA.Y, roomA.Y + roomA.Height));
	//		var pointB = new Point(Random.Range(roomB.X, roomB.X + roomB.Width), Random.Range(roomB.Y, roomB.Y + roomB.Height));
	//		while ((pointB.X != pointA.X) || (pointB.Y != pointA.Y))
	//		{
	//			if (pointB.X != pointA.X)
	//			{
	//				if (pointB.X > pointA.X)
	//					pointB.X--;
	//				else
	//					pointB.X++;
	//			}
	//			else if (pointB.Y != pointA.Y)
	//			{
	//				if (pointB.Y > pointA.Y)
	//					pointB.Y--;
	//				else
	//					pointB.Y++;
	//			}
	//			_world[pointB.X, pointB.Y] = 1;
	//		}
	//	}
	//}

	private void MakeCorridors(List<Room> roomList)
	{
		Room roomA = roomList[0];
		while (roomA != null)
		{
			//var roomB = (from A in roomList where A.ConnectedTo == null select A).FirstOrDefault();
			var roomB = FindClosestRoom(roomA, roomList, true);
			if (roomB == null) return;
			var pointA = new Point(Random.Range(roomA.X, roomA.X + roomA.Width), Random.Range(roomA.Y, roomA.Y + roomA.Height));
			var pointB = new Point(Random.Range(roomB.X, roomB.X + roomB.Width), Random.Range(roomB.Y, roomB.Y + roomB.Height));
			while ((pointB.X != pointA.X) || (pointB.Y != pointA.Y))
			{
				if (pointB.X != pointA.X)
				{
					if (pointB.X > pointA.X)
						pointB.X--;
					else
						pointB.X++;
				}
				else if (pointB.Y != pointA.Y)
				{
					if (pointB.Y > pointA.Y)
						pointB.Y--;
					else
						pointB.Y++;
				}
				_world[pointB.X, pointB.Y] = 1;
			}
			roomA.ConnectedTo = roomB;
			roomA = roomB;
		}
	}

	private void SquashRooms(List<Room> roomList)
	{
		for (var i = 0; i < 10; i++)
		{
			for (var j = 0; j < roomList.Count; j++)
			{
				var room = roomList[j];
				while (true)
				{
					var old_position = new Point(room.X, room.Y);
					if (room.X > 1) room.X--;
					if (room.Y > 1) room.Y--;
					if ((room.X == 1) && (room.Y == 1)) break;
					if (this.DoesCollide(roomList, room, j))
					{
						room.X = old_position.X;
						room.Y = old_position.Y;
						break;
					}
				}
			}
		}
	}

	private bool DoesCollide(List<Room> roomList, Room room, int ignore = -1)
	{
		for (var i = 0; i < roomList.Count; i++)
		{
			if (i == ignore) continue;
			var check = roomList[i];
			if (!((room.X + room.Width < check.X) || (room.X > check.X + check.Width) || (room.Y + room.Height < check.Y) || (room.Y > check.Y + check.Height)))
				return true;
		}
		return false;
	}

	private Room FindClosestRoom(Room room, List<Room> roomList, bool checkIfConnected = false)
	{
		var mid = new Point(room.X + (room.Width / 2), room.Y + (room.Height / 2));
		Room closest = null;
		var closest_distance = int.MaxValue;
		for (int i = 0; i < roomList.Count; i++)
		{
			var check = roomList[i];
			if (check == room) continue;
			if (checkIfConnected && check.ConnectedTo != null) continue;
			var check_mid = new Point(check.X + (check.Width / 2), check.Y + (check.Height / 2));
			var distance = Math.Min(Math.Abs(mid.X - check_mid.X) - (room.Width / 2) - (check.Width / 2), Math.Abs(mid.Y - check_mid.Y) - (room.Height / 2) - (check.Height / 2));
			if (distance < closest_distance)
			{
				closest_distance = distance;
				closest = check;
			}
		}
		return closest;
	}
}

public class Point
{
	public Point(int x, int y)
	{
		X = x;
		Y = y;
	}

	public int X { get; set; }
	public int Y { get; set; }
}

public class Room
{
	public Room(int x, int y, int w, int h)
	{
		X = x;
		Y = y;
		Width = w;
		Height = h;
	}

	public int X { get; set; }
	public int Y { get; set; }
	public int Width { get; set; }
	public int Height { get; set; }

	public int Left { get { return X; } }
	public int Right { get { return X + Width; } }
	public int Bottom { get { return Y + Height; } }
	public int Top { get { return Y; } }

	public Room ConnectedTo { get; set; }

	public override string ToString()
	{
		return X + ";" + Y + "{" + Width + ";" + Height + "}";
	}
}


public class RoomHelper
{
	private int[,] _world = null;
	public RoomHelper(int[,] world)
	{
		_world = world.Clone() as int[,];
	}

	public bool AreAllRoomsConnected(int x, int y)
	{
		FloodFill(x, y);
		return AreAllRoomsFilled();
	}

	private bool AreAllRoomsFilled()
	{
		for (int i = 0; i < _world.GetLength(0); i++)
			for (int j = 0; j < _world.GetLength(1); j++)
				if (_world[i, j] != 0)
					return false;
		return true;
	}

	private bool IsValidPosition(int x, int y)
	{
		if (x >= _world.GetLength(0)) return false;
		if (x < 0) return false;

		if (y >= _world.GetLength(1)) return false;
		if (y < 0) return false;

		return true;
	}


	void FloodFill(int x, int y)
	{
		if (!IsValidPosition(x, y)) return;

		if (_world[x, y] != 0)
			_world[x, y] = 0;
		else
			return;

		FloodFill(x + 1, y);
		FloodFill(x - 1, y);
		FloodFill(x, y + 1);
		FloodFill(x, y - 1);
	}
}